Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for SQLAlchemy 2.0 #249

Closed
wants to merge 16 commits into from
Closed

Conversation

rbuffat
Copy link
Contributor

@rbuffat rbuffat commented Feb 5, 2023

The aim of this PR is to add support for SQLAlchemy 2.0 Declarative Mapping. However, I'm neither familiar with the codebase of sqlacodegen nor the new SQLAlchemy 2.0 style. Thus, it might take time until this PR will be finished (if at all).

Thus, this PR is primarily is mainly created to make this work public for other people that want to contribute or build upon it.

@rbuffat rbuffat force-pushed the sqlalchemy2 branch 2 times, most recently from d25ccbc to 388ce49 Compare February 5, 2023 20:22
@rbuffat rbuffat marked this pull request as draft February 6, 2023 16:35
@@ -241,10 +243,9 @@ def add_import(self, obj: Any) -> None:

if type_.__name__ in dialect_pkg.__all__:
pkgname = dialect_pkgname
elif type_.__name__ in sqlalchemy.__all__: # type: ignore[attr-defined]

This comment was marked as outdated.

@rbuffat rbuffat changed the title Add support for SQLAlchemy 2.0 Declarative Mapping Add support for SQLAlchemy 2.0 Mar 2, 2023
@rbuffat
Copy link
Contributor Author

rbuffat commented Mar 2, 2023

@agronholm If you have time, it would be great if you could have a look at this PR.

The current state is that it implements the declarative, table and dataclasses generator for sqlachemy2. However, I need to test the generated models yet with a real database. The sqlmodel generator looks a bit incomplete (e.g. manytomany Relationships). Also sqlmodel seems to be not compatible with Sqlalchemy 2.0. Thus, I would rather not spend time on it.

There are some known issues:
Currently, the mapped_column includes the Column type. This would not be necessary. However, I'm not yet sure how well this works with type checker.

I marked some tests to require Python 3.9, as with this version standard collections can be used as type hints. Theoretically, adding from __future__ import annotations should allow this for older Python versions, but then validate_code() fails. Not yet sure why.

@rbuffat rbuffat marked this pull request as ready for review March 2, 2023 21:29
@amacfie
Copy link

amacfie commented Mar 2, 2023

I tested on this (with the dataclasses generator) and that issue seems to be fixed. (So I'm doubly looking forward to this being merged.) The output was this:

from typing import Optional

from sqlalchemy import ForeignKeyConstraint, PrimaryKeyConstraint, Uuid, text
from sqlalchemy.orm import DeclarativeBase, Mapped, MappedAsDataclass, mapped_column, relationship
import uuid

class Base(MappedAsDataclass, DeclarativeBase):
    pass


class Document(Base):
    __tablename__ = 'document'
    __table_args__ = (
        ForeignKeyConstraint(['image_id'], ['document.id'], ondelete='CASCADE', name='image_fk'),
        PrimaryKeyConstraint('id', name='document_pkey')
    )

    id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, server_default=text('gen_random_uuid()'))
    image_id: Mapped[Optional[uuid.UUID]] = mapped_column(Uuid)

    image: Mapped['Document'] = relationship('Document', remote_side=[id], back_populates='image_reverse')
    image_reverse: Mapped[list['Document']] = relationship('Document', remote_side=[image_id], back_populates='image')

A nitpick, but import uuid should be in the previous paragraph. Also, I'm curious if we need the PrimaryKeyConstraint since there's primary_key=True? I prefer Foo | None to Optional[Foo] but that's not a big deal.

@agronholm
Copy link
Owner

I just noticed the change from list to List, but with from __future__ import annotations, you can keep using list, yes?

@rbuffat
Copy link
Contributor Author

rbuffat commented Mar 3, 2023

@agronholm I first tried this approach but the test fails in exec() of validate_code(). See this branch: https://github.com/rbuffat/sqlacodegen/tree/try_annotations and the:

Error:

The above exception was the direct cause of the following exception:
tests/test_generator_declarative2.py:631: in test_manytomany_nobidi
    validate_code(
tests/conftest.py:16: in validate_code
    exec(generated_code, {})
<string>:10: in <module>
    ???
/opt/hostedtoolcache/Python/3.9.16/x64/lib/python3.9/site-packages/sqlalchemy/orm/decl_api.py:838: in __init_subclass__
    _as_declarative(cls._sa_registry, cls, cls.__dict__)
/opt/hostedtoolcache/Python/3.9.16/x64/lib/python3.9/site-packages/sqlalchemy/orm/decl_base.py:248: in _as_declarative
    return _MapperConfig.setup_mapping(registry, cls, dict_, None, {})
/opt/hostedtoolcache/Python/3.9.16/x64/lib/python3.9/site-packages/sqlalchemy/orm/decl_base.py:329: in setup_mapping
    return _ClassScanMapperConfig(
/opt/hostedtoolcache/Python/3.9.16/x64/lib/python3.9/site-packages/sqlalchemy/orm/decl_base.py:563: in __init__
    self._scan_attributes()
/opt/hostedtoolcache/Python/3.9.16/x64/lib/python3.9/site-packages/sqlalchemy/orm/decl_base.py:1005: in _scan_attributes
    collected_annotation = self._collect_annotation(
/opt/hostedtoolcache/Python/3.9.16/x64/lib/python3.9/site-packages/sqlalchemy/orm/decl_base.py:1233: in _collect_annotation
    extracted = _extract_mapped_subtype(
/opt/hostedtoolcache/Python/3.9.16/x64/lib/python3.9/site-packages/sqlalchemy/orm/util.py:2277: in _extract_mapped_subtype
    raise sa_exc.ArgumentError(
E   sqlalchemy.exc.ArgumentError: Could not interpret annotation Mapped[int].  Check that it uses names that are correctly imported at the module level. See chained stack trace for more hints.

The Sqlalchemy 1.4 code also uses typing.List. I assume it should be possible to upgrade the generated code using pyupgrade to the required Python version. Thus it should be ok to generate code in the style of the oldest by the project supported Python version.

@agronholm
Copy link
Owner

You need to merge from master, resolve the conflicts and fix the pre-commit errors.

tests/conftest.py Outdated Show resolved Hide resolved
@coveralls
Copy link

coveralls commented Mar 4, 2023

Coverage Status

Coverage: 97.564% (+0.1%) from 97.425% when pulling f5b6947 on rbuffat:sqlalchemy2 into 2828e46 on agronholm:master.

Comment on lines 47 to +48
"mysql-connector-python",
]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"mysql-connector-python",
]
"mysql-connector-python",
"sqlacodegen[sqlmodel]",
]

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will cause the sqlmodel tests to actually run, and restore the lost coverage.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh right. Hmm, those tests need to be run somehow.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you had the right idea, but couldn't you just have compared the sqlalchemy version to the string 1.4.* and conditionally installed sqlmodel then?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are many roads that lead to Rome. This way has the advantage that if one wants to pin SQLalchemy to a certain version only the install line needs to be changed.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not going to want to pin it to a specific version.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems resolved to me since bbfacf7, see lines 24-29

@rbuffat
Copy link
Contributor Author

rbuffat commented Mar 4, 2023

You need to merge from master, resolve the conflicts and fix the pre-commit errors.

The pre-commit check seemed broken, thus I didn't pay too much attention. It still seems to return different errors compared to what is in master, thus not sure how stable these checks are.

@agronholm
Copy link
Owner

I updated the pre-commit modules this Monday in master.

COVERALLS_FLAG_NAME: ${{ matrix.test-name }}
COVERALLS_PARALLEL: true
SQLALCHEMY_WARN_20: "true"
- uses: actions/checkout@v3
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only just noticed: what caused the reformatting? That makes it hard to see the actual changes.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you could undo the formatting changes and only leave the actual changes in, then I'm ready to merge.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formatting removed in #275

@@ -75,7 +78,8 @@ strict = true
plugins = ["sqlalchemy.ext.mypy.plugin"]

[tool.pytest.ini_options]
addopts = "-rsx --tb=short"
pythonpath = "src/sqlacodegen"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unnecessary.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in #275

@@ -75,7 +78,8 @@ strict = true
plugins = ["sqlalchemy.ext.mypy.plugin"]

[tool.pytest.ini_options]
addopts = "-rsx --tb=short"
pythonpath = "src/sqlacodegen"
addopts = "-rsx --tb=short --import-mode=importlib"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why --import-mode=importlib?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in #275

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rbuffat rbuffat marked this pull request as draft March 15, 2023 16:39
@agronholm
Copy link
Owner

I will soon resume work on this project, and would like to incorporate this PR ASAP. I have just a few questions/comments (above).

@rbuffat
Copy link
Contributor Author

rbuffat commented Mar 21, 2023

@agronholm I found some bugs, e.g. determining the type of collections. Moreover, after reflecting for some time, I think a major refactoring is needed to make the different generators independent, as currently, the code is getting unnecessarily complex.

I assume I will have time in the coming weeks to work more on the code.

@tomeszmh
Copy link

Any news on this one?

Thanks

@agronholm
Copy link
Owner

Not from my side. This project is pretty low on my priority list.

@mhauru
Copy link
Contributor

mhauru commented Jul 19, 2023

Hello @rbuffat and @agronholm! Our project depends on sqlacodegen (thanks for your work on it!) and we would like to upgrate to SQLAlchemy 2 for various reasons. Judging from the conversation above, this PR seems close to being mergeable. We could put some time into helping get this over the line, but for that it would be very helpful if you could summarise what is still needed. I see some comments by @agronholm above that seem relatively straight-forward, but @rbuffat also mentioned some bugs that they found. Is there a description of them somewhere? Also, there was a mention of a major refactor, that sounds to me like it would be a separate PR and not necessary to finish this, is that correct?

@agronholm
Copy link
Owner

I want this done too, but I've been busy with other projects. I still have one more project that I need to complete the next milestone on before I switch my attention to this one. As such, I welcome any efforts on this PR, but I can't devote any time on it just yet.

@mhauru
Copy link
Contributor

mhauru commented Aug 2, 2023

I made a commit addressing the comments you left @agronholm, see #275.

I think that covers all the explicit changes you requested. I'm a bit bothered by @rbuffat mentioning he found some bugs, but the tests don't pick them up and if @rbuffat isn't around to tell us more, I'm not sure what to do about them.

@rbuffat
Copy link
Contributor Author

rbuffat commented Aug 2, 2023

@mhauru As far as I remember, there are issues with name clashes between column names and imports. E.g. a column named uuid.

import uuid
from sqlalchemy import Uuid

class Test(Base):
   uuid: Mapped[uuid.UUID] = mapped_column(Uuid)

Additionally, there are issues with identifying the correct types e.g. for sqlalchemy.dialects.postgresql.JSONB

But the biggest issue is the current architecture. The different generators inherit from each other. With the current changes to support sqlalchemy 2.0 this increases the number of possible paths that can be taken and makes everything in my opinion unmaintainable. I would suggest making the generator not inheriting from each other and moving common functionality (e.g. determining Python types, field naming, importing of libraries out of the individual generators.

@agronholm
Copy link
Owner

Superseded by #275.

@agronholm agronholm closed this Aug 3, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants